package com.liyunc.demo.bundle.automation;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.liyunc.demo.comp.auth.AuthenticatedUser;
import com.liyunc.demo.comp.entity.base.AuditableEntity;
import com.liyunc.demo.comp.entity.base.LogicRemovableEntity;
import com.liyunc.demo.comp.entity.base.RowStatus;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.ibatis.reflection.MetaObject;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Transaction;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;

/**
 * @author liyuncong
 * @version 1.0
 * @file
 * @brief
 * @details
 * @date 2022-03-04
 *
 * Edit History
 * ----------------------------------------------------------------------------
 * DATE                     NAME               DESCRIPTION
 * 2022-03-04                       Created
 *
 * 自动根据当前安全上下文填充com.thundersoft.ar.comp.entity.base.AuditableEntity子类的以下属性
 * rowStatus
 * createdBy
 * createdAt
 * updatedBy
 * updatedAt
 *
 * 为了使此功能生效，需要在spring boot当中配置如下属性：
 * spring.jpa.properties.hibernate.ejb.interceptor=com.thundersoft.ar.bundles.hibernate.autofill.AuditableAutoFillInterceptor
 */
@Component
@Log4j2
public class AuditableAutofillInterceptor extends EmptyInterceptor implements MetaObjectHandler {

    private static final Pair<String, Object> DEFAULT_ROW_STATUS = Pair.of("rowStatus", 1);

    private boolean enabled() {
        AuditableAutofillConfigProperties configProperties =
            AuditableAutofillConfiguration.getAutofillConfigProperties();
        return !Objects.isNull(configProperties) && configProperties.getEnabled();
    }

    private AuthenticatedUser<?> currentUser() {
        return AuthenticatedUser
            .currentUser().orElse(new AuthenticatedUser<>("admin", "admin", "0"));
    }

    private boolean fillAuditable(Object auditable, String[] parameterNames, Object[] states) {
        return fillAuditable(auditable, currentUser(), parameterNames, states);
    }

    private boolean fillAuditable(Object entity, AuthenticatedUser<?> user, String[] parameterNames,
                                  Object[] states) {
        if (!enabled()) {
            return false;
        }
        if (!(entity instanceof LogicRemovableEntity removableEntity)) {
            return false;
        }
        List<Pair<String, Object>> values = new ArrayList<>(5);
        if (Objects.isNull(removableEntity.getRowStatus())) {
            values.add(DEFAULT_ROW_STATUS);
        }
        if (!(entity instanceof AuditableEntity)) {
            updateSates(values, parameterNames, states);
            return true;
        }
        LocalDateTime now = LocalDateTime.now();
        Long userId = Long.parseLong(user.getUserId());
        values.add(Pair.of("updatedAt", now));
        values.add(Pair.of("updatedBy", userId));
        values.add(Pair.of("createdAt", now));
        values.add(Pair.of("createdBy", userId));
        updateSates(values, parameterNames, states);
        return true;
    }

    private void updateSates(List<Pair<String, Object>> values, String[] parameters,
                             Object[] states) {
        IntStream
            .range(0, parameters.length)
            .mapToObj(index ->
                Pair.of(index,
                    values
                        .stream()
                        .filter(p -> p.getLeft().equalsIgnoreCase(parameters[index]))
                        .findFirst()
                        .orElse(null)))
            .filter(item -> Objects.nonNull(item.getRight()))
            .forEach(item -> states[item.getLeft()] = item.getRight().getRight());

    }

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames,
                          Type[] types) {
        return fillAuditable(entity, propertyNames, state);
    }

    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
                                Object[] previousState, String[] propertyNames, Type[] types) {
        return fillAuditable(entity, currentUser(), propertyNames, currentState);
    }

    @Override
    public void afterTransactionCompletion(Transaction tx) {
        if (tx.getStatus() == TransactionStatus.COMMITTED
            || tx.getStatus() == TransactionStatus.NOT_ACTIVE) {
            log.debug("afterTransactionCompletion invoke transactionHooks");
            AuditableAutofillConfiguration.getTransactionHookService().invokeAll();
        } else {
            AuditableAutofillConfiguration.getTransactionHookService().clear();
            log.debug(
                "afterTransactionCompletion will NOT "
                    + "invoke transactionHooks with transaction "
                    + "status {}",
                tx.getStatus());
        }
    }

    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "rowStatus", Integer.class,
            RowStatus.OK);
        this.fillAuditable(metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.fillAuditable(metaObject);
    }

    private void fillAuditable(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class,
            LocalDateTime.now());
        this.strictUpdateFill(metaObject, "updatedBy", Long.class,
            Long.parseLong(currentUser().getUserId()));
        this.strictUpdateFill(metaObject, "createdAt", LocalDateTime.class,
            LocalDateTime.now());
        this.strictUpdateFill(metaObject, "createdBy", Long.class,
            Long.parseLong(currentUser().getUserId()));
    }
}
